🐦 BIRD MIGRATION APP v.2.02 🐦

imports

# -.-|m { input: true, input_fold: hide}
#plotting
import matplotlib.pyplot as plt
#dataframe stuff
import numpy as np
import pandas as pd 
#a bunch of other shit i need
import re, os,random


#geo stuff
import cartopy.crs as ccrs
import geopandas as gpd
#panel & bokeh stuff
import holoviews as hv
import hvplot
from hvplot import pandas
import panel as pn
import bokeh.io
from panel.io import hold
from bokeh.themes.theme import Theme

bokeh.io.output_notebook();
pn.extension('mathjax')
hv.extension('bokeh');

#font
from matplotlib import font_manager
from markdown import markdown
from IPython.display import HTML

#ipython
from IPython.display import clear_output
clear_output()
# -.-|m { input: true, input_fold: hide}
#custom packages. IMPORT SEPRATELY DUE TO RATE LIMITNG. 
import birdspecies as bs #birdspecies.py
import color_swag as cswag #color_swag.py

GLOBALS

grabbing package items

url = bs.url
world = bs.world
bf = bs.bf
allspecies = bs.species
sp_info = bs.c_status

additional global variables

#GLOBAL MARKDOWN TABLE FORMAT 
#SO I DO NOT HAVE TO DO I/O EVERYTIME THE PLOT UPDATES. 
f = open("legendtemplate.txt", "r")
GLOBAL_TABLE_MARKDOWN = f.read()
f.close()

#monthz
GLOBAL_MONTHZ = {
    1:"January",
    2:"Febuary",
    3:"March",
    4:"April",
    5:"May",
    6:"June",
    7:"July",
    8:"August",
    9:"September",
    10:"October",
    11:"November",
    12:"December"
}

global css & style stuff

GLOBAL_PINK = "#FF69B4"
font_manager.fontManager.addfont(os.path.join("fonts", "Delius-Regular.ttf"))

theme = Theme(
    json={
    'attrs' : {
        'Text':
            {
                'text_font': 'Delius',
            },
        'Title': {
            'text_font': 'Delius',
            'text_color': '#FF69B4',
        },
    }
})
hv.renderer('bokeh').theme = theme

pn.config.raw_css.append("""
@font-face {
  font-family: Delius;
}
.sidebar {
    background: linear-gradient(135deg, #FF9EB3, #FFF954);
    border-radius: 12px;
    padding: 5px;
    margin-top: 10px;
    margin-bottom: 10px;
    margin-right: 10px;
    color: #D88CF8 !important;
    box-shadow: 0 0 10px rgba(245, 40, 145, 0.8);
    font-family: Delius; 
    --bokeh-mono-font: Delius;
}
.legendbox{
    background: linear-gradient(135deg, #FF9EB3, #FFF954);
    border-radius: 12px;
    padding: 5px;
    margin-top: 10px;
    margin-bottom: 10px;
    margin-right: 10px;
    color: #D88CF8 !important;
    box-shadow: 0 0 10px rgba(245, 40, 145, 0.8);
    font-family: Delius; 
    --bokeh-mono-font: Delius;
}
.blurb {
    font-size: 18px;
    line-height: 1.2em;
}
.swag {
    background-color: #FFFFFF;
    box-shadow: 0 0 10px rgba(245, 40, 145, 0.8);
}
.birb {
    fill: #D88CF8 !important;
}
""")
#WELCOME TO THIS INCREDIBLY COOKED SOLUTION. 
#so this makes a dictionary assigning a random pastel color for each bird species for 
#the world plot polygons when it is rendered. thats cuz i wanted colors to change by bird species
#but not individual data series plotting and also ididnt feel like asinging specific birds
GLOBAL_PLTCOLORZ = {allspecies[i]:random.choice(cswag.pastels) for i in range(len(allspecies))}

computational

route calculater

def route(species, c): 
    """gets all the routes for this bird species and when he traveled."""
#THIS NEEDS TO BE LESS COOKED. i litrlyy dont think it needs the species argument at all
#rework when i get the chance. 
    locs = bf[(bf['Bird species'] == species) & (bf['Migratory route codes'] == c)]       
    return [locs["GPS_xx"].tolist(), locs["GPS_yy"].tolist(), locs["Migration start year"].tolist()]

plot elements

def limits(mimax, coors): 
    """gets the minimum and maximum values for the x and y axes so we can zoom in"""
    for i, v in enumerate(coors):
#ummmmmmmmmmmmmmmmmmm theoretically i could condense this code to make it less shit. 
#and i will. 
#at some point. 
        if i%2 == 0 and v < mimax[i]: 
            mimax[i] = v
        elif i%2 != 0 and v > mimax[i]: 
            mimax[i] = v
    return mimax
def startmonth(yrs):
    """gets the start month of the migration for each year-- for the legend"""
    ms = []
    for y in yrs: 
        ms.append(bf[(bf["Migration start year"]==y) & 
           (bf["Bird species"] == targetspecies.value) &
                    (bf["Migration nodes"] == "Origin")]["Migration start month"].unique())
    return ms

renderers

def rendermap(unique_codes, specie, yrs):
    """re-renders the map"""
    fig, ax = plt.subplots()
    world.plot(ax=ax, color=random.choice(cswag.pastels), alpha=0.75)
    mimax = [1000, -1000, 1000, -1000]
    for i, code in enumerate(unique_codes): 
        rte = route(specie, code)

        mimax = limits(mimax, [min(rte[0]), max(rte[0]), min(rte[1]), max(rte[1])])
        try: 
            chosencolor = random.choice(cswag.plot_colors[cswag.rainbow[yrs.index(rte[2][0])%6]])
        except: 
    #this is for that annoying fucking issue w years i talked abt in the updateyears 
    #func. we ball tho
            chosencolor = "black"
        ax.plot(rte[0], rte[1], marker='.', linewidth=1, markersize=3,color=chosencolor)
        ax.scatter(rte[0][0], rte[1][0], marker='*', 
                   s=20, color=chosencolor, label=f"Origin, Route{i}")
        ax.scatter(rte[0][-1], rte[1][-1], marker='o', 
                   s=20, color=chosencolor, label=f"End, Route{i}")
            
    #zooms in like 2 the main parts of the route. idk i might change it to +-0.5
    ax.set_ylim(mimax[2]-0.5, mimax[3]+0.5)
    ax.set_xlim(mimax[0]-0.5, mimax[1]+0.5)
    #we make it pink and such
    yassify(ax, specie)
    fig.tight_layout();
    plt.close(fig)
    return fig
def maphv(unique_codes, specie, yrs): 
    """this renders the map but on holoviz so it's not static. didnt help the rendering
    speed issue tho
    lmao"""
    rteplots = []
    mimax = [1000, -1000, 1000, -1000]
    for i, code in enumerate(unique_codes): 
        rt = pd.DataFrame(np.column_stack(route(specie, code)), 
                           columns=["x", "y", "ye"])
#this is to labelj if the stops are origns or destinations otherwise itll put 
#transit 
        stops = {0:"Origin", len(rt):"Destination"}
#usign a ternary operator + a dictionary bc i wanted a ternary um idk but couldn't do 
#three options w it???? idk this feels like the most memory inefficient way to possibly
#do this so idk. we'll see 
#also this is for making the label that will hover 
        rt["hover"] = [f"Year: {rt["ye"][p]:.0f}\n{stops[p]
            if (p == 0) or (p == len(rt)) else "Transit"}" for p in range(len(rt))]
        mimax = limits(mimax, [min(rt["x"]), max(rt["x"]), min(rt["y"]), max(rt["y"])])
#therse some fucking issue with years occasionally being slightly different due to 
#time passing and year overlap and such. idk. i need to figure out a less cooked
#solution than this. thsi is temperorary. 
        try: 
            chosencolor = random.choice(cswag.plot_colors[cswag.rainbow[yrs.index(int(rt["ye"][0]))%6]])
        except: 
            chosencolor = "black"
#ok so this needs both hover_cols and hover_tooltips to work. hover_col is set 
#as the columnthat i custom made earlier and it also needs hover_tooltips to make th e
#additional lable of x y diseppear. idk. lol
        rteplot = rt.hvplot.points(x="x", y="y", projection = ccrs.PlateCarree(), geo=True, 
                                   color=chosencolor, 
                                   hover_cols=["hover"]).opts(hover_tooltips=[("", "@hover")])
        rteplots.append(rteplot)
#OK. so this is cloning world & custom resizing in the clone itself
    mp = world.hvplot.polygons(color=GLOBAL_PLTCOLORZ[specie], xlim=(mimax[0]-0.5, mimax[1]+0.5),
                        ylim=(mimax[2]-0.5, mimax[3]+0.5), alpha=0.7, 
                               projection = ccrs.PlateCarree()) 
#THE ONLY WAY that this overlay will ascutlly fucking work 
#is if you set the same projection
#for both being PlateCarree. its so evil bro. 
    for r in rteplots:
        mp = mp * r
    return mp 
def update_yrs_blrb_pic(event):
    """this modifies the checkbox for years, when the bird changes, to show only the years that 
    we have data for that bird. also it updates the picture """
#NOTE: sometimes some weird shit happens where the same route can have different
#migration start dates so 
#uhhhhhhhhh
#i'll prolly change to a range when i have more time to gaf about such things 
    new_species = event.new
    new_years= sorted(int(y) for y in pd.unique(
        bf.loc[bf['Bird species'] == new_species, 'Migration start year']
    ).tolist())
    #this is not just a years updater any more
    #nts change function name to reflect
    
    blurblabels.value = bs.get_blurb(new_species)
    birb.object = f"bird_pics/{new_species}.jpg"
    yearcheck.options = new_years  
    
    
    conservation = bf.loc[bf['Bird species'] == new_species, 
                               'The IUCN Red List (2023)'].tolist()[0]
    bitext.object = conservation
#i made conservation colors for each one its in a dictionary in bridspecies.py so like
#if it was critically endangered it'd be red.
#then the box of the legend switches to a gradient
#between pink and that color. 
    legendbox.styles = {'background': f"linear-gradient(135deg, #FF9EB3, {sp_info[conservation][1]}"}
    constatus.value = [bicon, bitext]
    yearcheck.value = [new_years[0]]
def update_legend(event): 
    """updates the legend!!!!!!!!!!!!!! *the object itself 
    by calling a markdown update function"""
    newyrs = event.new
    leglabels.object = superultramd(newyrs)

local aesthetic stuff

def yassify(ax, species): 
    """yassifies the graph idk it needed to be cuntier"""
    ax.set_title(f"Routes for {species}", color=GLOBAL_PINK)
    ax.set_xlabel("Longitude", color=GLOBAL_PINK)
    ax.set_ylabel("Latitude", color=GLOBAL_PINK)
    ax.tick_params(axis='x',  color=GLOBAL_PINK)
    ax.tick_params(axis='y',  color=GLOBAL_PINK)
    for spine in ax.spines.values():
        spine.set_edgecolor(pink)
        spine.set_color(pink)
        spine.set_linewidth(1.2)
        ax.grid(True, alpha=0.2, linestyle="--")
def superultramd(yrs): 
    """edits table data for the legend on the side of the plot."""
#we clone the global markdown which is pulled form the .txt doc at the beginning
    mdwn = GLOBAL_TABLE_MARKDOWN
    
    stmonths = startmonth(yrs)
#we iteratively added the lines about months, yr etc for each plot that gets created.
    for i, y in enumerate(yrs): 
        mdwn += f"\n| {y} | {cswag.rainbow[i%6]} | {(", ".join([GLOBAL_MONTHZ[x] for x in stmonths[i]]))} |"
    return mdwn

display objects

#INITIALIZING ALL THE OBJECTS FOR THE DISPLAY. 
#note these are individual objects not yet in columns yet. 
#the columnifying will come later
targetspecies = pn.widgets.Select(options=allspecies, value=allspecies[0])
yearcheck = pn.widgets.CheckBoxGroup(name='Years', value=[2001], options=[])
blurblabels = pn.widgets.indicators.String(value="Info:", font_size= "16pt")
leglabels = pn.pane.Markdown(object=superultramd([])) 
bicon = pn.pane.SVG('bird.svg', width=150, css_classes=["birb"])
bitext = pn.pane.Markdown('', styles={'font_size':'26pt', 'color':'white'})
constatus = pn.GridBox(*[bicon, bitext], ncols=2, nrows=1)
birb = pn.pane.Image(f"bird_pics/{targetspecies.value}.jpg")

    
#watcher(s) for changes in our species. THIS IS A HUGE FUCKING PAIN IN THE ASS. 
#IF THE APP CRASHES IT'S PROBABLY DUE TO THIS. JUST CLEAR ALL OUTPUTS AND RERUN.
targetspecies.param.watch(update_yrs_blrb_pic, ["value"], onlychanged=True)
yearcheck.param.watch(update_legend, ["value"], onlychanged=True);

object depends

#so ive done a couple different dependency things w panel. #1 is a watcher whicch 
#u can see above. also just a @pn.depends thing which is what we learned in class. 
#idk how efficient it is mixing them. overall not thrilled 
#w/ the panel app... it's usable but laggy. idk. we'll see. 

@pn.depends(targetspecies.param.value, yearcheck.param.value)
def yearfilterable(species, yrs):
    """allows you to filter the routes by year and graph accordingly."""
#making a smaller dataset based on the years... yf... idk it stands for yearframe i guess
    specie = species[0] if isinstance(species, (list, tuple, np.ndarray, pd.Series)) else species
    yf =  bf[(bf['Bird species'] == specie) & (bf['Migration start year'].isin(yrs))]
    unique_codes = yf[yf['Bird species'] == specie]["Migratory route codes"].unique()
#fig will get resized according to dimensions but this gives it a good baseline 
#to start w so we end up a w larger graph
#closing the previous plot to save space and such
    plt.close()
    return(maphv(unique_codes, specie, yrs))
    

rows

#stack bird species ontop of its corresponding pic
species_col = pn.Column("# BIRD SPECIES", targetspecies, birb, width=400, css_classes=["sidebar"]);
#yrs and blurb are in the same row aswell.
years_col   = pn.Column("# YEAR(S)", yearcheck, width=180, css_classes=["sidebar"]);
blurb = pn.Column("# ADDITIONAL INFO", blurblabels, css_classes=["blurb", "sidebar"]);

legendbox = pn.Column(leglabels, constatus, css_classes=["sidebar"])
row1 = pn.Row(
    species_col,
    years_col,
    blurb,
    sizing_mode="stretch_width"
);
row2 = pn.Row(
    legendbox,
    pn.Column(yearfilterable, sizing_mode="stretch_width"),
    
    sizing_mode="stretch_width"
);

creates the overall app

#initializes in a new tab. i'll make it servable at some point so u can run from cmd. 
app = pn.Column(
    row1,
    row2,
    width=1000,
    sizing_mode="stretch_height"
)

runs the app

update_yrs_blrb_pic(type('e', (), {'new': targetspecies.value})())
update_legend(type('e', (), {'new': yearcheck.value})())
app.show(title="SWAG");
Launching server at http://localhost:34223